package org.apache.commons.beanutils;

import static gu.sql2java.utils.CaseSupport.*;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.beanutils.expression.DefaultResolver;
import org.apache.commons.beanutils.expression.Resolver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.google.common.base.Function;
import com.google.common.base.Throwables;

import gu.sql2java.BaseRow;
import gu.sql2java.RowMetaData;
import gu.sql2java.excel.config.PropertyConfig;
import gu.sql2java.excel.utils.FieldUtils;
/**
 * 基于{@link PropertyUtilsBean}实现对类的私有字段的读写{@link FieldPropertyDescriptor}的支持,
 * 以及对{@link gu.sql2java.BaseRow}的读写支持
 * 
 * @author guyadong
 *
 */
public class MorePropertyUtilsBean extends PropertyUtilsBean {
    /** Log instance */
    private final Log log = LogFactory.getLog(MorePropertyUtilsBean.class);
    private final Resolver resolver = new DefaultResolver();
	private final Function<String, PropertyConfig> configSupplier;
    /** An empty object array */
    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
    private static final ThreadLocal<String> curentNestedName=new ThreadLocal<>();
    
    public MorePropertyUtilsBean(){
    	this(null);
    }
	public MorePropertyUtilsBean(Function<String, PropertyConfig>configSupplier) {
		super();
		this.configSupplier = configSupplier;
	}

	/**
	 * 当调用父类方法返回为{@code null}时根据bean的类型尝试返回对应的{@link PropertyDescriptor}实例<br>
	 * 如果bean为{@link BaseRow}则返回{@link BaseRowPropertyDescriptor}实例,
	 * 如果能在bean的类中找到同名的成员({@link Field}),返回{@link FieldPropertyDescriptor},
	 * 这个过程中如果没有找到指定的成员,会自动尝试对字段名进行驼峰命名(camel-case)和蛇形命名(snake-case)转换来查找成员.
	 * @see org.apache.commons.beanutils.PropertyUtilsBean#getPropertyDescriptor(java.lang.Object, java.lang.String)
	 */
	@Override
	public PropertyDescriptor getPropertyDescriptor(Object bean, String name)
			throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
		PropertyDescriptor descriptor =  super.getPropertyDescriptor(bean, name);
		if(null == descriptor){
			try {
				if(bean instanceof BaseRow ){
					RowMetaData metaData = ((BaseRow)bean).fetchMetaData();
					if(metaData.columnIDOf(name) >= 0){
						return new BaseRowPropertyDescriptor(metaData, name);
					}
				}
				Field field = FieldUtils.getField(bean.getClass(), name,true);
				if(null == field){
					if(isSnakelcase(name)){
						field = FieldUtils.getField(bean.getClass(), toCamelcase(name),true);
					}else if(isCamelcase(name)){						
						field = FieldUtils.getField(bean.getClass(), toSnakecase(name),true);
					}
				}
				if(null != field){
					return new FieldPropertyDescriptor(field);
				}
			} catch (IntrospectionException e) {
				throw new RuntimeException(e);
			}
		}
		return descriptor;
	}

	@Override
	public Method getWriteMethod(Class<?> clazz, PropertyDescriptor descriptor) {
		if(descriptor instanceof MorePropertyDescriptor){
			return descriptor.getWriteMethod();
		}
		if(null != configSupplier){
			PropertyConfig config = configSupplier.apply(curentNestedName.get());
			String methodName = config.getColumnConfig().getWriteMethod();
			if(!methodName.isEmpty()){
				try {
					Field field = FieldUtils.getField(clazz, descriptor.getName(),true);
					return clazz.getMethod(methodName,
							null != field ? field.getType() : descriptor.getPropertyType());
				} catch (Exception e) {
					Throwables.throwIfUnchecked(e);
					throw new RuntimeException(e);
				}
			}
		}
        return super.getWriteMethod(clazz, descriptor);
	}

	@Override
	Method getReadMethod(Class<?> clazz, PropertyDescriptor descriptor) {
		if(descriptor instanceof MorePropertyDescriptor){
			return descriptor.getReadMethod();
		}
		if(null != configSupplier){
			PropertyConfig config = configSupplier.apply(curentNestedName.get());
			String methodName = config.getColumnConfig().getReadMethod();
			if(!methodName.isEmpty()){
				try {
					return clazz.getMethod(methodName);
				} catch (Exception e) {
					Throwables.throwIfUnchecked(e);
					throw new RuntimeException(e);
				}
			}
		}
		return super.getReadMethod(clazz, descriptor);
	}

	/**
	 * 主要代码复制自{@link PropertyUtilsBean#getSimpleProperty(Object, String)}<br>
	 * 不同只在于invokeMethod方法改为调用{@link #invokeMethod(Method, Object, String, Object[])}
	 */
	@Override
	public Object getSimpleProperty(Object bean, String name)
			throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        if (bean == null) {
            throw new IllegalArgumentException("No bean specified");
        }
        if (name == null) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }

        // Validate the syntax of the property name
        if (resolver.hasNested(name)) {
            throw new IllegalArgumentException
                    ("Nested property names are not allowed: Property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        } else if (resolver.isIndexed(name)) {
            throw new IllegalArgumentException
                    ("Indexed property names are not allowed: Property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        } else if (resolver.isMapped(name)) {
            throw new IllegalArgumentException
                    ("Mapped property names are not allowed: Property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        }

        // Handle DynaBean instances specially
        if (bean instanceof DynaBean) {
            final DynaProperty descriptor =
                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
            if (descriptor == null) {
                throw new NoSuchMethodException("Unknown property '" +
                        name + "' on dynaclass '" +
                        ((DynaBean) bean).getDynaClass() + "'" );
            }
            return (((DynaBean) bean).get(name));
        }

        // Retrieve the property getter method for the specified property
        final PropertyDescriptor descriptor =
                getPropertyDescriptor(bean, name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on class '" + bean.getClass() + "'" );
        }
        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
        if (readMethod == null) {
            throw new NoSuchMethodException("Property '" + name +
                    "' has no getter method in class '" + bean.getClass() + "'");
        }

        // Call the property getter and return the value
        final Object value = invokeMethod(readMethod, bean,name, EMPTY_OBJECT_ARRAY);
        return (value);

    }
	/**
	 * 主要代码复制自{@link PropertyUtilsBean#setSimpleProperty(Object, String, Object)}<br>
	 * 不同只在于invokeMethod方法改为调用{@link #invokeMethod(Method, Object, String, Object[])}
	 */
	@Override
	public void setSimpleProperty(Object bean, String name, Object value)
			throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        if (bean == null) {
            throw new IllegalArgumentException("No bean specified");
        }
        if (name == null) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }

        // Validate the syntax of the property name
        if (resolver.hasNested(name)) {
            throw new IllegalArgumentException
                    ("Nested property names are not allowed: Property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        } else if (resolver.isIndexed(name)) {
            throw new IllegalArgumentException
                    ("Indexed property names are not allowed: Property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        } else if (resolver.isMapped(name)) {
            throw new IllegalArgumentException
                    ("Mapped property names are not allowed: Property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        }

        // Handle DynaBean instances specially
        if (bean instanceof DynaBean) {
            final DynaProperty descriptor =
                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
            if (descriptor == null) {
                throw new NoSuchMethodException("Unknown property '" +
                        name + "' on dynaclass '" +
                        ((DynaBean) bean).getDynaClass() + "'" );
            }
            ((DynaBean) bean).set(name, value);
            return;
        }

        // Retrieve the property setter method for the specified property
        final PropertyDescriptor descriptor =
                getPropertyDescriptor(bean, name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on class '" + bean.getClass() + "'" );
        }
        final Method writeMethod = getWriteMethod(bean.getClass(), descriptor);
        if (writeMethod == null) {
            throw new NoSuchMethodException("Property '" + name +
                    "' has no setter method in class '" + bean.getClass() + "'");
        }

        // Call the property setter method
        final Object[] values = new Object[1];
        values[0] = value;
        if (log.isTraceEnabled()) {
            final String valueClassName =
                value == null ? "<null>" : value.getClass().getName();
            log.trace("setSimpleProperty: Invoking method " + writeMethod
                      + " with value " + value + " (class " + valueClassName + ")");
        }
        invokeMethod(writeMethod, bean, name,values);

    }
	
    @Override
	public Object getNestedProperty(Object bean, String name)
			throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        if (bean == null) {
            throw new IllegalArgumentException("No bean specified");
        }
        if (name == null) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }
        curentNestedName.set("");
        try {
			
        	// Resolve nested references
        	while (resolver.hasNested(name)) {
        		String next = resolver.next(name);
        		Object nestedBean = null;
        		bean = tryParseJsonIfString(bean);
        		if (bean instanceof Map) {
        			AtomicReference<String> ref = new AtomicReference<>(next);
        			nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, ref);
        			if(!next.equals(ref.get())){
        				next = ref.get();
        			}
        		} else if (resolver.isMapped(next)) {
        			nestedBean = getMappedProperty(bean, next);
        		} else if (resolver.isIndexed(next)) {
        			nestedBean = getIndexedProperty(bean, next);
        		} else {
        			concat(next);
        			nestedBean = getSimpleProperty(bean, next);
        		}
        		if (nestedBean == null) {
        			return null;
        		}
        		bean = nestedBean;
        		name = resolver.remove(name);
        	}
        	concat(name);
    		bean = tryParseJsonIfString(bean);
        	if (bean instanceof Map) {
    			bean = getPropertyOfMapBean((Map<?, ?>) bean, new AtomicReference<>(name));
        	} else if (resolver.isMapped(name)) {
        		bean = getMappedProperty(bean, name);
        	} else if (resolver.isIndexed(name)) {
        		bean = getIndexedProperty(bean, name);
        	} else {
        		bean = getSimpleProperty(bean, name);
        	}
        	return bean;
		} finally {
			curentNestedName.remove();
		}

    }
    private void concat(String next){
    	String n = curentNestedName.get();
		if(!n.isEmpty()){
			n = n.concat(".");
		}
		n = n.concat(next);
		curentNestedName.set(n);
    }
	/**
	 * 读取{@link Map}中{@code propertyName}定义的值<br>
	 * 如果不存在{@code propertyName}定义的key,
	 * 尝试将{@code propertyName}转为驼峰命名(camel-case)或蛇形命名(snake-case)的key再试<br>
	 * 如果找到了则将转换后的字段名保存在{@code propertyName}
	 * @param bean Map Object
	 * @param propertyName [inout] property name
	 * @return value or null 
	 */
	private Object getPropertyOfMapBean(Map<?, ?> bean, AtomicReference<String>propertyName)
			throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
		
		if(bean.containsKey(propertyName.get())){
			return super.getPropertyOfMapBean(bean, propertyName.get());
		}
		
		Object value = null;
		if(isCamelcase(propertyName.get())){
			String p = toSnakecase(propertyName.get());
			if(bean.containsKey(p)){
				value = bean.get(p);
				propertyName.set(p);
			}
		}else if(isSnakelcase(propertyName.get())){
			String p = toSnakecase(propertyName.get());
			if(bean.containsKey(p)){
				value = bean.get(p);
				propertyName.set(p);
			}
		}
		return value;
	}
	/** 
     * 对{@link #invokeMethod(Method, Object, Object[])}进一步封装，支持{@link FieldPropertyDescriptor}和{@link BaseRowPropertyDescriptor}<br>
     * 当输入的方法对象自来{@link FieldPropertyDescriptor}或{@link BaseRowPropertyDescriptor}时
     * 使用 {@link FieldPropertyDescriptor}或{@link BaseRowPropertyDescriptor}提供读写字段方法要求的参数调用
     * @see FieldPropertyDescriptor#readMethod(Object, String)
     * @see FieldPropertyDescriptor#writeMethod(Object, String, Object)
     */
    private Object invokeMethod(
                        final Method method,
                        final Object bean,
                        final String name,
                        final Object[] values)throws
						    IllegalAccessException,
						    InvocationTargetException{
    	if(MorePropertyDescriptor.class.isAssignableFrom(method.getDeclaringClass())){
    		return method.invoke(null, net.gdface.utils.ArrayUtils.addAll(new Object[]{bean,name}, values));
    	}
    	return invokeMethod(method,bean,values);
    }
	
    /** 
     * This just catches and wraps IllegalArgumentException.
     * copy from  {@link PropertyUtilsBean}
     */
    private Object invokeMethod(
                        final Method method,
                        final Object bean,
                        final Object[] values)
                            throws
                                IllegalAccessException,
                                InvocationTargetException {
        if(bean == null) {
            throw new IllegalArgumentException("No bean specified " +
                "- this should have been checked before reaching this method");
        }

        try {

            return method.invoke(bean, values);

        } catch (final NullPointerException cause) {
            // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is
            // null for a primitive value (JDK 1.5+ throw IllegalArgumentException)
            String valueString = "";
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    if (i>0) {
                        valueString += ", " ;
                    }
                    if (values[i] == null) {
                        valueString += "<null>";
                    } else {
                        valueString += (values[i]).getClass().getName();
                    }
                }
            }
            String expectedString = "";
            final Class<?>[] parTypes = method.getParameterTypes();
            if (parTypes != null) {
                for (int i = 0; i < parTypes.length; i++) {
                    if (i > 0) {
                        expectedString += ", ";
                    }
                    expectedString += parTypes[i].getName();
                }
            }
            final IllegalArgumentException e = new IllegalArgumentException(
                "Cannot invoke " + method.getDeclaringClass().getName() + "."
                + method.getName() + " on bean class '" + bean.getClass() +
                "' - " + cause.getMessage()
                // as per https://issues.apache.org/jira/browse/BEANUTILS-224
                + " - had objects of type \"" + valueString
                + "\" but expected signature \""
                +   expectedString + "\""
                );
            if (!BeanUtils.initCause(e, cause)) {
                log.error("Method invocation failed", cause);
            }
            throw e;
        } catch (final IllegalArgumentException cause) {
            String valueString = "";
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    if (i>0) {
                        valueString += ", " ;
                    }
                    if (values[i] == null) {
                        valueString += "<null>";
                    } else {
                        valueString += (values[i]).getClass().getName();
                    }
                }
            }
            String expectedString = "";
            final Class<?>[] parTypes = method.getParameterTypes();
            if (parTypes != null) {
                for (int i = 0; i < parTypes.length; i++) {
                    if (i > 0) {
                        expectedString += ", ";
                    }
                    expectedString += parTypes[i].getName();
                }
            }
            final IllegalArgumentException e = new IllegalArgumentException(
                "Cannot invoke " + method.getDeclaringClass().getName() + "."
                + method.getName() + " on bean class '" + bean.getClass() +
                "' - " + cause.getMessage()
                // as per https://issues.apache.org/jira/browse/BEANUTILS-224
                + " - had objects of type \"" + valueString
                + "\" but expected signature \""
                +   expectedString + "\""
                );
            if (!BeanUtils.initCause(e, cause)) {
                log.error("Method invocation failed", cause);
            }
            throw e;

        }
    }
    
    private Object  tryParseJsonIfString(Object bean) {
    	if(bean instanceof String){
    		try {
    			return JSON.parseObject((String)bean);
    		} catch (JSONException e) {
    			throw new IllegalArgumentException(String.format("INVALID String %s for bean",bean));
    		}
    	}
    	return bean;
    }
}
